/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.ComponentModel.Design;

namespace DaveSexton.DocToMaml
{
	internal sealed partial class ProgramForm : Form
	{
		#region Public Properties
		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public string StartupProjectFile
		{
			get
			{
				return startupProjectFile;
			}
			set
			{
				if (string.IsNullOrEmpty(value))
					startupProjectFile = null;
				else
					startupProjectFile = value;
			}
		}

		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public bool IsDirty
		{
			get
			{
				return project.IsDirty;
			}
			set
			{
				project.IsDirty = value;
			}
		}

		[Browsable(false)]
		public MessageBoxOptions MessageBoxOptions
		{
			get
			{
				if (RightToLeft == RightToLeft.Yes)
					return MessageBoxOptions.RightAlign | MessageBoxOptions.RtlReading;
				else
					return (MessageBoxOptions) 0;
			}
		}
		#endregion

		#region Private / Protected
		private string startupProjectFile;
		private bool convertingAsync, selectingInputs, inputListViewIndexNotChanging, inputListViewIndexChanged;
		private FormWindowState windowStateBeforeMinimizeToTray;
		private readonly ProjectManager project;
		private readonly ProgramServicesContainer serviceContainer;
		private readonly ProgramServices services;
		private readonly ProgramSettings settings;
		private readonly ComponentResourceManager formResources = new ComponentResourceManager(typeof(ProgramForm));
		private readonly ProgramFormUIStateManager uiStateManager;
		private readonly Dictionary<RuleAction, List<RuleMatchKind>> actionMatchKinds = new Dictionary<RuleAction, List<RuleMatchKind>>();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ProgramForm" /> class.
		/// </summary>
		public ProgramForm()
		{
			InitializeComponent();

			settings = new ProgramSettings(this);
			services = new ProgramServices(this);
			serviceContainer = new ProgramServicesContainer(services);

			project = new ProjectManager(this, services);
			project.Opened += project_Opened;
			project.Closing += project_Closing;
			project.Closed += project_Closed;
			project.Saved += project_Saved;
			project.IsDirtyChanged += project_IsDirtyChanged;

			uiStateManager = new ProgramFormUIStateManager(this);
			uiStateManager.ResetToolStrips();
			uiStateManager.UpdateInputListViewColumn();

			foreach (int value in Enum.GetValues(typeof(RuleAction)))
				newRuleActionComboBox.Items.Add((RuleAction) value);

			foreach (int value in Enum.GetValues(typeof(Maml.MamlElementType)))
				if (value > 0)
					newRuleMamlComboBox.Items.Add((Maml.MamlElementType) value);

			List<RuleMatchKind> matchKinds = new List<RuleMatchKind>();
			actionMatchKinds[RuleAction.Remove] = matchKinds;

			matchKinds.Add(RuleMatchKind.Attribute);
			matchKinds.Add(RuleMatchKind.ElementId);
			matchKinds.Add(RuleMatchKind.StyleSheetClass);
			matchKinds.Add(RuleMatchKind.TagName);

			matchKinds = new List<RuleMatchKind>();
			actionMatchKinds[RuleAction.Replace] = matchKinds;

			matchKinds.Add(RuleMatchKind.Attribute);
			matchKinds.Add(RuleMatchKind.TagName);

			matchKinds = new List<RuleMatchKind>();
			actionMatchKinds[RuleAction.Convert] = matchKinds;

			matchKinds.Add(RuleMatchKind.ElementId);
			matchKinds.Add(RuleMatchKind.StyleSheetClass);
			matchKinds.Add(RuleMatchKind.TagName);

			InitializeNewRule();
		}
		#endregion

		#region Methods
		private bool EnsureHelp(bool canPromptUser)
		{
			string lastHelpFile = settings.HelpFile;
			string currentHelpFile = help.HelpNamespace;

			if (string.IsNullOrEmpty(currentHelpFile))
				currentHelpFile = lastHelpFile;

			if (File.Exists(currentHelpFile))
			{
				SetHelpFile(currentHelpFile);
				return true;
			}
			else if (!string.Equals(currentHelpFile, lastHelpFile, StringComparison.OrdinalIgnoreCase)
				&& File.Exists(lastHelpFile))
			{
				SetHelpFile(lastHelpFile);
				return true;
			}
			else if (canPromptUser)
			{
				string initialDirectory = Application.StartupPath;

			Prompt:
				using (OpenFileDialog dialog = new OpenFileDialog())
				{
					dialog.CheckFileExists = true;
					dialog.DefaultExt = ".chm";
					dialog.FileName = ProgramSettings.DefaultHelpFileName;
					dialog.InitialDirectory = initialDirectory;
					dialog.Filter = Resources.Text.CompiledHelpFileDialogFilter;
					dialog.Title = Resources.Text.OpenHelpFile;

					if (dialog.ShowDialog(this) == DialogResult.OK)
					{
						string file = dialog.FileName;

						// NOTE: Only a .CHM file is allowed, for security.
						// During testing, if an assembly was selected instead of a .CHM file, it was 'opened' automatically 
						// by the Help class; i.e., Help doesn't seem to ensure that the specified help namespace is an actual help file.
						if (!string.Equals(Path.GetExtension(file), ".chm", StringComparison.OrdinalIgnoreCase))
						{
							initialDirectory = Path.GetDirectoryName(file);
							goto Prompt;
						}

						SetHelpFile(file);
						return true;
					}
				}
			}

			return false;
		}

		private void SetHelpFile(string helpFile)
		{
			help.HelpNamespace = helpFile;

			if (new Uri(helpFile).Equals(new Uri(ProgramSettings.DefaultHelpFile)))
				settings.HelpFile = null;
			else
				settings.HelpFile = helpFile;
		}

		public bool SetDirtyIf(bool predicate)
		{
			if (predicate)
				IsDirty = true;

			return predicate;
		}

		public bool CreateProject()
		{
			return project.OpenNew();
		}

		public bool OpenProject()
		{
			return project.Open();
		}

		public bool OpenProject(string projectFile)
		{
			return project.Open(projectFile);
		}

		public void SaveProject()
		{
			uiStateManager.UpdateProjectBeforeSave();

			project.Save();
		}

		public void SaveProjectAs()
		{
			uiStateManager.UpdateProjectBeforeSave();

			project.SaveAs();
		}

		public bool CloseProject(bool forceSave)
		{
			return project.Close(forceSave);
		}

		private void SaveSettings()
		{
			settings.Save();
		}

		private void LoadSettings()
		{
			settings.Load();
		}

		private void HideToTray()
		{
			Visible = false;
			ShowInTaskbar = false;
		}

		private void ShowFromTray()
		{
			Visible = true;
			ShowInTaskbar = true;
			WindowState = windowStateBeforeMinimizeToTray;
		}

		private void OpenResultsFolder()
		{
			if (!string.IsNullOrEmpty(project.LastOutputDirectory))
				System.Diagnostics.Process.Start(project.LastOutputDirectory);
		}

		private void DeleteSelectedInputs()
		{
			if (services.UIService.ShowMessage(Resources.Text.ConfirmDeleteInputs, Resources.Text.ConfirmDeleteInputsCaption,
				MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
			{
				SetDirtyIf(inputListView.SelectedItems.Count > 0);

				status.Text = "Removing selected files...";

				statusStrip.Refresh();

				inputListView.BeginUpdate();

				try
				{
					foreach (ListViewItem item in inputListView.SelectedItems)
					{
						IInput input = (IInput) item.Tag;

						try
						{
							project.Inputs.Remove(input);

							inputListView.Items.Remove(item);
						}
						finally
						{
							input.Dispose();
						}
					}
				}
				finally
				{
					inputListView.EndUpdate();

					status.Text = project.FilePath;

					convertToolStripButton.Enabled = inputListView.Items.Count > 0;
					convertAllToolStripMenuItem.Enabled = convertToolStripButton.Enabled;
				}
			}
		}

		private void AddFileInput()
		{
			using (OpenFileDialog dialog = new OpenFileDialog())
			{
				dialog.Title = Resources.Text.AddFileInputDialogTitle;
				dialog.DefaultExt = ".htm";
				dialog.Filter = Resources.Text.AddFileInputDialogFilter;
				dialog.Multiselect = true;
				dialog.InitialDirectory = System.IO.Path.GetDirectoryName(project.FilePath);

				if (dialog.ShowDialog(this) == DialogResult.OK)
				{
					status.Text = "Listing files...";

					statusStrip.Refresh();

					inputListView.BeginUpdate();

					try
					{
						string[] files = dialog.FileNames;

						SetDirtyIf(files.Length > 0);

						foreach (string file in files)
							if (!project.Inputs.Contains(file))
								project.Inputs.Add(AddFileInput(file));
					}
					finally
					{
						inputListView.EndUpdate();

						status.Text = project.FilePath;
					}
				}
			}
		}

		private FileInput AddFileInput(string file)
		{
			FileInput input = project.CreateFileInput(file);

			AddFileInput(input);

			return input;
		}

		private void AddFileInput(IInput input)
		{
			ListViewItem item = inputListView.Items.Add(input.Name, "File");
			item.ToolTipText = input.FullPath;
			item.Tag = input;

			convertToolStripButton.Enabled = true;
			convertAllToolStripMenuItem.Enabled = true;
		}

		private void AddFolderInput()
		{
			using (FolderBrowserDialog dialog = new FolderBrowserDialog())
			{
				dialog.Description = Resources.Text.AddFolderInputDialogDescription;
				dialog.SelectedPath = System.IO.Path.GetDirectoryName(project.FilePath);

				if (dialog.ShowDialog(this) == DialogResult.OK)
				{
					string path = dialog.SelectedPath;

					inputListView.BeginUpdate();

					try
					{
						project.Inputs.Add(AddFolderInput(path));

						IsDirty = true;
					}
					finally
					{
						inputListView.EndUpdate();
					}
				}
			}
		}

		private FolderInput AddFolderInput(string path)
		{
			FolderInput input = project.CreateFolderInput(path);

			AddFolderInput(input);

			return input;
		}

		private void AddFolderInput(IInput input)
		{
			ListViewItem item = inputListView.Items.Add(input.Name, "Folder");
			item.ToolTipText = input.FullPath;
			item.Tag = input;

			convertToolStripButton.Enabled = true;
			convertAllToolStripMenuItem.Enabled = true;
		}

		private void ConvertAllInputs()
		{
			string target = GetTargetFolderName();

			if (target == null)
				return;

			if (Directory.GetFiles(target, "*.xml", SearchOption.TopDirectoryOnly).Length > 0)
			{
				switch (services.UIService.ShowMessage(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Text.ConfirmClearOutputDirectory, Environment.NewLine), Resources.Text.ConfirmClearOutputDirectoryCaption,
					MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question))
				{
					case DialogResult.Cancel:
						return;
					case DialogResult.Yes:
						System.IO.Directory.Delete(target, true);
						System.IO.Directory.CreateDirectory(target);
						break;
				}
			}

			uiStateManager.SetUserCommandStateForConversion(true);

			convertingAsync = true;

			conversionStatus.Text = string.Empty;
			conversionStatus.Visible = true;

			ConvertAllInputs(target);
		}

		private void ConvertAllInputs(string target)
		{
			BackgroundWorker worker = new BackgroundWorker();
			worker.WorkerReportsProgress = true;

			ConversionContext context = new ConversionContext(project.Project);

			context.BeforeConvertInput += (o, e) =>
			{
				worker.ReportProgress(e.ProgressPercentage, "Converting " + e.Input.Name + "...");
			};

			worker.DoWork += (o, e) =>
			{
				context.ConvertAllInputs(target);

				if (context.ConvertedFilesCount > 0)
					project.Images.CreateConceptualArtFile(target);
			};

			worker.ProgressChanged += (o, e) =>
			{
				conversionStatus.Text = (string) e.UserState;
				progress.Value = e.ProgressPercentage;
			};

			worker.RunWorkerCompleted += (o, e) =>
			{
				convertingAsync = false;

				uiStateManager.SetUserCommandStateForConversion(false);

				conversionStatus.Text = string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Text.ConversionStatus, context.ConvertedFilesCount, context.ErrorCount);

				if (context.ErrorCount > 0)
					FindFirstConversionError();
			};

			worker.RunWorkerAsync();
		}

		private void FindFirstConversionError()
		{
			foreach (IInput input in project.Inputs)
			{
				if (input.ConversionErrors.Count > 0)
				{
					contentTabs.SelectedTab = logTabPage;
					uiStateManager.SelectedInput = input;
					break;
				}
			}
		}

		private string GetTargetFolderName()
		{
			using (FolderBrowserDialog dialog = new FolderBrowserDialog())
			{
				dialog.Description = Resources.Text.ConvertAllTargetFolderDialogDescription;
				dialog.SelectedPath = (!string.IsNullOrEmpty(project.LastOutputDirectory))
					? project.LastOutputDirectory
					: System.IO.Path.GetDirectoryName(project.FilePath);

				if (dialog.ShowDialog(this) == DialogResult.OK)
					return dialog.SelectedPath;
				else
					return null;
			}
		}

		private void InitializeNewRule()
		{
			newRuleActionComboBox.SelectedItem = RuleAction.Remove;
			newRuleMatchKindComboBox.SelectedItem = RuleMatchKind.TagName;
			newRuleMatchTextBox.Text = null;
			newRuleMamlComboBox.SelectedIndex = -1;
			newRuleReplacementTextBox.Text = null;
		}

		private void AddNewRule()
		{
			IRule rule = ValidateNewRule();

			if (rule != null)
			{
				try
				{
					rulesBindingSource.Add(rule);
				}
				catch (ArgumentException)
				{
					errors.SetError(newRuleMatchTextBox, Resources.Errors.RulesErrorNotUnique);
					return;
				}

				rulesDataGridView.ClearSelection();
				rulesDataGridView.Rows[rulesDataGridView.RowCount - 1].Selected = true;

				InitializeNewRule();
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]		// the 'value' variable is reassigned multiple times, so multiple casts are necessary
		private IRule ValidateNewRule()
		{
			errors.SetError(newRuleMatchKindComboBox, null);
			errors.SetError(newRuleMatchTextBox, null);
			errors.SetError(newRuleMamlComboBox, null);
			errors.SetError(newRuleReplacementTextBox, null);

			bool hasError = false;

			string match = newRuleMatchTextBox.Text.Trim();

			if (match.Length == 0)
			{
				errors.SetError(newRuleMatchTextBox, Resources.Errors.RulesErrorMatchRequired);
				hasError = true;
			}
			else
				errors.SetError(newRuleMatchTextBox, null);

			RuleAction action = (RuleAction) newRuleActionComboBox.SelectedItem;
			RuleMatchKind matchKind = (RuleMatchKind) newRuleMatchKindComboBox.SelectedItem;
			Maml.MamlElementType? maml = (newRuleMamlComboBox.SelectedIndex == -1) ? null : (Maml.MamlElementType?) newRuleMamlComboBox.SelectedItem;
			string replacement = newRuleReplacementTextBox.Text.Trim();

			switch (action)
			{
				case RuleAction.Remove:
					if (hasError)
						return null;
					else
						return new RemoveRule(match, matchKind);
				case RuleAction.Replace:
					if (matchKind == RuleMatchKind.ElementId || matchKind == RuleMatchKind.StyleSheetClass)
					{
						errors.SetError(newRuleMatchKindComboBox, Resources.Errors.RulesErrorMatchKindInvalid);
						hasError = true;
					}
					else
						errors.SetError(newRuleMatchKindComboBox, null);

					if (replacement.Length == 0)
					{
						errors.SetError(newRuleReplacementTextBox, Resources.Errors.RulesErrorReplacementRequired);
						hasError = true;
					}
					else
						errors.SetError(newRuleReplacementTextBox, null);

					if (hasError)
						return null;
					else
						return new ReplaceRule(match, matchKind, replacement);
				case RuleAction.Convert:
					if (matchKind == RuleMatchKind.Attribute)
					{
						errors.SetError(newRuleMatchKindComboBox, Resources.Errors.RulesErrorMatchKindInvalid);
						hasError = true;
					}
					else
						errors.SetError(newRuleMatchKindComboBox, null);

					if (!maml.HasValue)
					{
						errors.SetError(newRuleMamlComboBox, Resources.Errors.RulesErrorMamlRequired);
						hasError = true;
					}
					else if (maml.Value == Maml.MamlElementType.Default)
					{
						errors.SetError(newRuleMamlComboBox, Resources.Errors.RulesErrorMamlInvalid);
						hasError = true;
					}
					else
						errors.SetError(newRuleMamlComboBox, null);

					if (hasError)
						return null;
					else
						return new ConvertRule(match, matchKind, maml.Value);
				default:
					throw new InvalidOperationException();
			}
		}

		private void UpdateNewRuleForAction()
		{
			RuleAction action = (RuleAction) newRuleActionComboBox.SelectedItem;
			RuleMatchKind matchKind = (RuleMatchKind) ((newRuleMatchKindComboBox.SelectedIndex == -1)
				? RuleMatchKind.TagName
				: newRuleMatchKindComboBox.SelectedItem);

			newRuleMatchKindComboBox.Items.Clear();

			foreach (RuleMatchKind value in actionMatchKinds[action])
				newRuleMatchKindComboBox.Items.Add(value);

			newRuleMatchKindComboBox.SelectedItem = matchKind;

			if (newRuleMatchKindComboBox.SelectedIndex == -1)
				newRuleMatchKindComboBox.SelectedItem = RuleMatchKind.TagName;

			switch (action)
			{
				case RuleAction.Remove:
					newRuleMamlComboBox.Enabled = false;
					newRuleMamlComboBox.SelectedIndex = -1;
					newRuleReplacementTextBox.Text = null;
					newRuleReplacementTextBox.Enabled = false;
					break;
				case RuleAction.Replace:
					newRuleMamlComboBox.Enabled = false;
					newRuleMamlComboBox.SelectedIndex = -1;
					newRuleReplacementTextBox.Enabled = true;
					break;
				case RuleAction.Convert:
					newRuleMamlComboBox.Enabled = true;
					newRuleMamlComboBox.SelectedIndex = 0;
					newRuleReplacementTextBox.Text = null;
					newRuleReplacementTextBox.Enabled = false;
					break;
			}
		}

		private bool ShowHelp()
		{
			if (EnsureHelp(true))
			{
				Help.ShowHelp(this, help.HelpNamespace);
				return true;
			}

			return false;
		}

		private bool ShowHelp(HelpNavigator navigator)
		{
			if (EnsureHelp(true))
			{
				if (navigator == HelpNavigator.Find)
					Help.ShowHelp(this, help.HelpNamespace, navigator, string.Empty);
				else
					Help.ShowHelp(this, help.HelpNamespace, navigator);

				return true;
			}

			return false;
		}

		private bool ShowHelp(HelpNavigator navigator, object parameter)
		{
			if (EnsureHelp(true))
			{
				Help.ShowHelp(this, help.HelpNamespace, navigator, parameter ?? string.Empty);
				return true;
			}

			return false;
		}

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				if (components != null)
					components.Dispose();

				if (serviceContainer != null)
					serviceContainer.Dispose();

				if (services != null)
					services.Dispose();

				project.Dispose();
			}
			base.Dispose(disposing);
		}
		#endregion

		#region Event Handlers
		protected override void OnLoad(EventArgs e)
		{
			LoadSettings();

			EnsureHelp(false);

			base.OnLoad(e);
		}

		protected override void OnShown(EventArgs e)
		{
			if (!string.IsNullOrEmpty(startupProjectFile))
				OpenProject(startupProjectFile);
			else
			{
				switch (settings.Startup)
				{
					case StartupProject.OpenProject:
						if (!string.IsNullOrEmpty(settings.ProjectDirectory))
							OpenProject(settings.ProjectDirectory);
						else
							OpenProject();
						break;
					case StartupProject.OpenLastProject:
						if (!string.IsNullOrEmpty(settings.LastProject))
							OpenProject(settings.LastProject);
						else
							goto case 0;
						break;
					case StartupProject.NewProject:
						CreateProject();
						break;
				}
			}

			base.OnShown(e);
		}

		protected override void OnFormClosing(FormClosingEventArgs e)
		{
			if (!e.Cancel)
			{
				if (e.CloseReason == CloseReason.UserClosing && settings.MinimizeToTray && settings.MinimizeOnClose)
				{
					e.Cancel = true;
					WindowState = FormWindowState.Minimized;
				}
				else if (convertingAsync)
				{
					if (services.UIService.ShowMessage(string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Resources.Text.ConfirmCloseDuringConversion, Environment.NewLine), Resources.Text.ConfirmCloseDuringConversionCaption,
						MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == DialogResult.Cancel)
					{
						e.Cancel = true;
					}
				}
				else
				{
					bool forceSave = e.CloseReason == CloseReason.WindowsShutDown || e.CloseReason == CloseReason.TaskManagerClosing;

					if (project.Loaded && !CloseProject(forceSave))
						e.Cancel = true;
					else
						SaveSettings();
				}
			}

			base.OnFormClosing(e);
		}

		protected override void OnResize(EventArgs e)
		{
			base.OnResize(e);

			if (WindowState == FormWindowState.Minimized && settings.MinimizeToTray)
				HideToTray();
			else
				windowStateBeforeMinimizeToTray = WindowState;
		}

		private void project_Closing(object sender, CancelEventArgs e)
		{
			if (project.Loaded && !project.IsNew)
				settings.LastProject = project.FilePath;
		}

		private void project_Closed(object sender, EventArgs e)
		{
			uiStateManager.UpdateUIForProject();
		}

		private void project_Opened(object sender, EventArgs e)
		{
			uiStateManager.UpdateUIForProject();
		}

		private void project_Saved(object sender, EventArgs e)
		{
			uiStateManager.UpdateUIAfterSave();
		}

		private void project_IsDirtyChanged(object sender, EventArgs e)
		{
			// project.LastOutputDirectory may be assigned in a worker thread when a conversion starts, in which case the following call must be marshaled to the UI thread.
			if (InvokeRequired)
			{
				Invoke((MethodInvoker) delegate()
				{
					uiStateManager.UpdateUIForDirtyState();
				});
			}
			else
				uiStateManager.UpdateUIForDirtyState();
		}

		private void inputsSplitContainer_SplitterMoved(object sender, SplitterEventArgs e)
		{
			uiStateManager.UpdateInputListViewColumn();
		}

		private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
		{
			using (OptionsDialog dialog = new OptionsDialog(settings))
			{
				if (dialog.ShowDialog(this) == DialogResult.OK)
					settings.Save();
			}
		}

		private void exitToolStripMenuItem_Click(object sender, EventArgs e)
		{
			Application.Exit();
		}

		private void notifyExitToolStripMenuItem_Click(object sender, EventArgs e)
		{
			Application.Exit();
		}

		private void newToolStripMenuItem_Click(object sender, EventArgs e)
		{
			CreateProject();
		}

		private void newToolStripButton_Click(object sender, EventArgs e)
		{
			CreateProject();
		}

		private void openToolStripMenuItem_Click(object sender, EventArgs e)
		{
			OpenProject();
		}

		private void openToolStripButton_Click(object sender, EventArgs e)
		{
			OpenProject();
		}

		private void saveToolStripMenuItem_Click(object sender, EventArgs e)
		{
			SaveProject();
		}

		private void saveToolStripButton_Click(object sender, EventArgs e)
		{
			SaveProject();
		}

		private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
		{
			SaveProjectAs();
		}

		private void closeToolStripMenuItem_Click(object sender, EventArgs e)
		{
			CloseProject(false);
		}

		private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
		{
			using (AboutForm form = new AboutForm())
			{
				form.ShowDialog(this);
			}
		}

		private void ProgramForm_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.KeyCode == Keys.F1 && string.IsNullOrEmpty(help.HelpNamespace))
				// HelpProvider ignores F1 when HelpNamespace hasn't been set.
				ShowHelp(HelpNavigator.TableOfContents);
		}

		private void contentsToolStripMenuItem_Click(object sender, EventArgs e)
		{
			ShowHelp(HelpNavigator.TableOfContents);
		}

		private void indexToolStripMenuItem_Click(object sender, EventArgs e)
		{
			ShowHelp(HelpNavigator.Index);
		}

		private void searchToolStripMenuItem_Click(object sender, EventArgs e)
		{
			ShowHelp(HelpNavigator.Find);
		}

		private void helpToolStripButton_Click(object sender, EventArgs e)
		{
			ShowHelp();
		}

		private void notifyShowToolStripMenuItem_Click(object sender, EventArgs e)
		{
			ShowFromTray();
		}

		private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
		{
			ShowFromTray();
		}

		private void deleteInputToolStripButton_Click(object sender, EventArgs e)
		{
			DeleteSelectedInputs();
		}

		private void addFileToolStripMenuItem_Click(object sender, EventArgs e)
		{
			AddFileInput();
		}

		private void addFolderToolStripMenuItem_Click(object sender, EventArgs e)
		{
			AddFolderInput();
		}

		private void inputListView_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Control && e.KeyCode == Keys.A)
			{
				e.Handled = true;

				inputListView.BeginUpdate();
				selectingInputs = true;

				try
				{
					foreach (ListViewItem item in inputListView.Items)
						item.Selected = true;
				}
				finally
				{
					selectingInputs = false;
					inputListView.EndUpdate();
				}

				uiStateManager.UpdateForSelectedInput();
			}
		}

		private void editor_HtmlChanged(object sender, DaveSexton.Controls.HtmlChangedEventArgs e)
		{
			switch (e.Reason)
			{
				case DaveSexton.Controls.HtmlChangedReason.UserEdit:
				case DaveSexton.Controls.HtmlChangedReason.CommandNotification:
					System.Diagnostics.Debug.WriteLine("HTML changed.", "ProgramForm Source View");

					uiStateManager.CommitSourceChanges();
					break;
				case DaveSexton.Controls.HtmlChangedReason.DocumentLoaded:
					System.Diagnostics.Debug.WriteLine("HTML document loaded.", "ProgramForm Source View");

					editor.IsDirty = false;
					break;
			}
		}

		private void sourceResultChangedWarningButton_Click(object sender, EventArgs e)
		{
			uiStateManager.EnableSourceEditing();
		}

		private void inputListView_SelectedIndexChanged(object sender, EventArgs e)
		{
			// NOTE: ListView.SelectedIndexChanged is raised twice when an item is already selected; the first is for
			// the "unselecting" of the current item and the next is for the "selecting" of the new item.  Since this
			// behavior is entirely not helpful, inputListViewIndexNotChanging is set in the MouseDown event to let this
			// event handler know whether the first inovcation should be ignored.

			if (inputListViewIndexNotChanging)
				inputListViewIndexNotChanging = false;
			else if (!selectingInputs)
				uiStateManager.UpdateForSelectedInput();
			else
				inputListViewIndexChanged = true;
		}

		private void inputListView_MouseDown(object sender, MouseEventArgs e)
		{
			selectingInputs = true;

			if (inputListView.GetItemAt(e.X, e.Y) != null && inputListView.SelectedItems.Count > 0)
				inputListViewIndexNotChanging = true;
		}

		private void inputListView_MouseUp(object sender, MouseEventArgs e)
		{
			selectingInputs = false;

			if (inputListViewIndexChanged)
			{
				inputListViewIndexChanged = false;
				uiStateManager.UpdateForSelectedInput();
			}
			else if (inputListView.SelectedIndices.Count == 0 && project.Loaded)
				// whitespace was clicked after the current selection was clicked more than once in a row.
				uiStateManager.UpdateForSelectedInput();
		}

		private void tabs_SelectedIndexChanged(object sender, EventArgs e)
		{
			uiStateManager.UpdateForSelectedTab();
		}

		private void resultTextBox_TextChanged(object sender, EventArgs e)
		{
			uiStateManager.UpdateStateForResultTextChanged();
		}

		private void resultTextBox_Validating(object sender, CancelEventArgs e)
		{
			e.Cancel = !uiStateManager.CommitResultChanges();
		}

		private void resultTextBox_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Control && e.KeyCode == Keys.A)
			{
				e.Handled = true;
				resultTextBox.SelectAll();
			}
		}

		private void logTextBox_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Control && e.KeyCode == Keys.A)
			{
				e.Handled = true;
				logTextBox.SelectAll();
			}
		}

		private void imagesTextBox_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Control && e.KeyCode == Keys.A)
			{
				e.Handled = true;
				imagesTextBox.SelectAll();
			}
		}

		private void convertToolStripButton_Click(object sender, EventArgs e)
		{
			ConvertAllInputs();
		}

		private void convertAllToolStripMenuItem_Click(object sender, EventArgs e)
		{
			ConvertAllInputs();
		}

		private void openResultsFolderToolStripMenuItem_Click(object sender, EventArgs e)
		{
			OpenResultsFolder();
		}

		private void openResultsFolderToolStripButton_Click(object sender, EventArgs e)
		{
			OpenResultsFolder();
		}

		private void rulesDataGridView_CellEnter(object sender, DataGridViewCellEventArgs e)
		{
			// This event handler is a workaround for an EditOnEnter bug that prevents rows from being selected when a header is clicked.
			// This code is based on the workaround found here: https://connect.microsoft.com/VisualStudio/feedback/Workaround.aspx?FeedbackID=98504

			rulesDataGridView.EditMode = DataGridViewEditMode.EditOnEnter;
		}

		private void rulesDataGridView_MouseClick(object sender, MouseEventArgs e)
		{
			// This event handler is a workaround for an EditOnEnter bug that prevents rows from being selected when a header is clicked.
			// This code is based on the workaround found here: https://connect.microsoft.com/VisualStudio/feedback/Workaround.aspx?FeedbackID=98504

			DataGridView.HitTestInfo hit = rulesDataGridView.HitTest(e.X, e.Y);

			if (hit.Type == DataGridViewHitTestType.RowHeader)
			{
				rulesDataGridView.EditMode = DataGridViewEditMode.EditOnKeystrokeOrF2;
				rulesDataGridView.EndEdit();
			}
		}

		private void rulesBindingSource_ListChanged(object sender, ListChangedEventArgs e)
		{
			// this event is raised while designer initialization code is being executed so uiStateManager will be null
			if (uiStateManager != null)
				uiStateManager.UpdateForRulesDataChanges();
		}

		private void newRuleButton_Click(object sender, EventArgs e)
		{
			AddNewRule();
		}

		private void newRuleActionComboBox_SelectedIndexChanged(object sender, EventArgs e)
		{
			UpdateNewRuleForAction();
		}

		private void rulesDataGridView_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
		{
			if (e.RowIndex > -1)
			{
				for (int r = 0; r < e.RowCount; r++)
				{
					DataGridViewRow row = rulesDataGridView.Rows[r + e.RowIndex];
					IRule rule = (IRule) row.DataBoundItem;

					switch (rule.Action)
					{
						case RuleAction.Replace:
							row.Cells[replacementDataGridViewTextBoxColumn.Index].Value = rule.Value;
							break;
						case RuleAction.Convert:
							row.Cells[mamlDataGridViewTextBoxColumn.Index].Value = rule.Value;
							break;
					}
				}
			}
		}
		#endregion

		#region Nested
		internal abstract partial class UIStateManagerBase { }
		internal sealed partial class ProgramFormUIStateManager { }

		private sealed class ProgramServicesContainer : Container
		{
			#region Public Properties
			#endregion

			#region Private / Protected
			private readonly IServiceProvider services;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="ProgramServicesContainer" /> class.
			/// </summary>
			public ProgramServicesContainer(IServiceProvider services)
			{
				if (services == null)
					throw new ArgumentNullException("services");

				this.services = services;
			}
			#endregion

			#region Methods
			protected override object GetService(Type serviceType)
			{
				object service = services.GetService(serviceType);

				if (service != null)
					return service;
				else
					return base.GetService(serviceType);
			}
			#endregion
		}
		#endregion
	}
}